import type { Event, EventEnvelope, EventItem, SeverityLevel } from '@sentry/core';
import { createEnvelope, debug } from '@sentry/core';
import * as RN from 'react-native';
import type { Spec } from '../src/js/NativeRNSentry';
import type { ReactNativeOptions } from '../src/js/options';
import { base64StringFromByteArray, utf8ToBytes } from '../src/js/vendor';
import { NATIVE } from '../src/js/wrapper';

jest.mock('react-native', () => {
  let initPayload: ReactNativeOptions | null = null;

  const RNSentry: Spec = {
    addBreadcrumb: jest.fn(),
    captureEnvelope: jest.fn(),
    clearBreadcrumbs: jest.fn(),
    crashedLastRun: jest.fn(),
    crash: jest.fn(),
    fetchNativeDeviceContexts: jest.fn(() =>
      Promise.resolve({
        someContext: {
          someValue: 0,
        },
      }),
    ),
    fetchNativeRelease: jest.fn(() =>
      Promise.resolve({
        build: '1.0.0.1',
        id: 'test-mock',
        version: '1.0.0',
      }),
    ),
    setContext: jest.fn(),
    setExtra: jest.fn(),
    setTag: jest.fn(),
    setUser: jest.fn(() => {
      return;
    }),
    initNativeSdk: jest.fn(options => {
      initPayload = options;

      return Promise.resolve(true);
    }),
    closeNativeSdk: jest.fn(() => Promise.resolve()),
    // @ts-expect-error for testing.
    _getLastPayload: () => ({ initPayload }),
    startProfiling: jest.fn(),
    stopProfiling: jest.fn(),
  };

  return {
    NativeModules: {
      RNSentry,
    },
    Platform: {
      OS: 'ios',
    },
  };
});

const RNSentry = RN.NativeModules.RNSentry as Spec;

const callAllScopeMethods = () => {
  NATIVE.addBreadcrumb({
    message: 'test',
    data: {
      map: { a: 1 },
      array: [1, 2, 3],
      unique: 123,
    },
  });
  NATIVE.clearBreadcrumbs();
  NATIVE.setUser({
    id: 'setUser',
  });
  NATIVE.setTag('key', 'value');
  NATIVE.setContext('key', {
    value: 'value',
    data: {
      map: { a: 1 },
      array: [1, 2, 3],
      unique: 123,
    },
  });
  NATIVE.setExtra('key', 'value');
};

describe('Tests Native Wrapper', () => {
  beforeEach(() => {
    NATIVE.platform = 'ios';
    NATIVE.enableNative = true;
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('startWithOptions', () => {
    test('calls native module', async () => {
      await NATIVE.initNativeSdk({
        dsn: 'test',
        enableNative: true,
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });

      expect(RNSentry.initNativeSdk).toHaveBeenCalled();
    });

    test('warns if there is no dsn', async () => {
      debug.warn = jest.fn();

      await NATIVE.initNativeSdk({
        enableNative: true,
        defaultSidecarUrl: undefined,
        devServerUrl: undefined,
        mobileReplayOptions: undefined,
      });

      expect(RNSentry.initNativeSdk).not.toHaveBeenCalled();
      expect(debug.warn).toHaveBeenLastCalledWith(
        'Warning: No DSN was provided. The Sentry SDK will be disabled. Native SDK will also not be initalized.',
      );
    });

    test('does not call native module with enableNative: false', async () => {
      debug.warn = jest.fn();

      await NATIVE.initNativeSdk({
        dsn: 'test',
        enableNative: false,
        enableNativeNagger: true,
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });

      expect(RNSentry.initNativeSdk).not.toHaveBeenCalled();
      expect(NATIVE.enableNative).toBe(false);
      expect(debug.warn).toHaveBeenLastCalledWith('Note: Native Sentry SDK is disabled.');
    });

    test('filter beforeSend when initializing Native SDK', async () => {
      await NATIVE.initNativeSdk({
        dsn: 'test',
        enableNative: true,
        autoInitializeNativeSdk: true,
        beforeSend: jest.fn(),
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });

      expect(RNSentry.initNativeSdk).toHaveBeenCalled();
      // @ts-expect-error mock value
      const initParameter = RNSentry.initNativeSdk.mock.calls[0][0];
      expect(initParameter).not.toHaveProperty('beforeSend');
      expect(NATIVE.enableNative).toBe(true);
    });

    test('filter beforeBreadcrumb when initializing Native SDK', async () => {
      await NATIVE.initNativeSdk({
        dsn: 'test',
        enableNative: true,
        autoInitializeNativeSdk: true,
        beforeBreadcrumb: jest.fn(),
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });

      expect(RNSentry.initNativeSdk).toHaveBeenCalled();
      // @ts-expect-error mock value
      const initParameter = RNSentry.initNativeSdk.mock.calls[0][0];
      expect(initParameter).not.toHaveProperty('beforeBreadcrumb');
      expect(NATIVE.enableNative).toBe(true);
    });

    test('filter beforeSendTransaction when initializing Native SDK', async () => {
      await NATIVE.initNativeSdk({
        dsn: 'test',
        enableNative: true,
        autoInitializeNativeSdk: true,
        beforeSendTransaction: jest.fn(),
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });

      expect(RNSentry.initNativeSdk).toHaveBeenCalled();
      // @ts-expect-error mock value
      const initParameter = RNSentry.initNativeSdk.mock.calls[0][0];
      expect(initParameter).not.toHaveProperty('beforeSendTransaction');
      expect(NATIVE.enableNative).toBe(true);
    });

    test('filter beforeSendMetric when initializing Native SDK', async () => {
      await NATIVE.initNativeSdk({
        dsn: 'test',
        enableNative: true,
        autoInitializeNativeSdk: true,
        beforeSendMetric: jest.fn(),
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });

      expect(RNSentry.initNativeSdk).toHaveBeenCalled();
      // @ts-expect-error mock value
      const initParameter = RNSentry.initNativeSdk.mock.calls[0][0];
      expect(initParameter).not.toHaveProperty('beforeSendMetric');
      expect(NATIVE.enableNative).toBe(true);
    });

    test('filter integrations when initializing Native SDK', async () => {
      await NATIVE.initNativeSdk({
        dsn: 'test',
        enableNative: true,
        autoInitializeNativeSdk: true,
        integrations: [],
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });

      expect(RNSentry.initNativeSdk).toHaveBeenCalled();
      // @ts-expect-error mock value
      const initParameter = RNSentry.initNativeSdk.mock.calls[0][0];
      expect(initParameter).not.toHaveProperty('integrations');
      expect(NATIVE.enableNative).toBe(true);
    });

    test('does not initialize with autoInitializeNativeSdk: false', async () => {
      NATIVE.enableNative = false;
      debug.warn = jest.fn();

      await NATIVE.initNativeSdk({
        dsn: 'test',
        enableNative: true,
        autoInitializeNativeSdk: false,
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });

      expect(RNSentry.initNativeSdk).not.toHaveBeenCalled();
      expect(NATIVE.enableNative).toBe(true);

      // Test that native bridge methods will go through
      callAllScopeMethods();
      expect(RNSentry.addBreadcrumb).toHaveBeenCalledWith({
        message: 'test',
        data: {
          map: { a: 1 },
          array: [1, 2, 3],
          unique: 123,
        },
      });
      expect(RNSentry.clearBreadcrumbs).toHaveBeenCalled();
      expect(RNSentry.setUser).toHaveBeenCalledWith(
        {
          id: 'setUser',
        },
        {},
      );
      expect(RNSentry.setTag).toHaveBeenCalledWith('key', 'value');
      expect(RNSentry.setContext).toHaveBeenCalledWith('key', {
        value: 'value',
        data: {
          map: { a: 1 },
          array: [1, 2, 3],
          unique: 123,
        },
      });
      expect(RNSentry.setExtra).toHaveBeenCalledWith('key', 'value');
    });

    test('enableNative: false takes precedence over autoInitializeNativeSdk: false', async () => {
      debug.warn = jest.fn();

      await NATIVE.initNativeSdk({
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
        dsn: 'test',
        enableNative: false,
        autoInitializeNativeSdk: false,
      });

      expect(RNSentry.initNativeSdk).not.toHaveBeenCalled();
      expect(NATIVE.enableNative).toBe(false);

      // Test that native bridge methods will NOT go through
      callAllScopeMethods();
      expect(RNSentry.addBreadcrumb).not.toHaveBeenCalled();
      expect(RNSentry.clearBreadcrumbs).not.toHaveBeenCalled();
      expect(RNSentry.setUser).not.toHaveBeenCalled();
      expect(RNSentry.setTag).not.toHaveBeenCalled();
      expect(RNSentry.setContext).not.toHaveBeenCalled();
      expect(RNSentry.setExtra).not.toHaveBeenCalled();
    });

    test('sets ignoreErrorsStr and ignoreErrorsRegex correctly when ignoreErrors contains strings and regex', async () => {
      const regex1 = /foo/;
      const regex2 = new RegExp('bar');
      await NATIVE.initNativeSdk({
        dsn: 'test',
        enableNative: true,
        ignoreErrors: ['string1', regex1, 'string2', regex2],
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });
      expect(RNSentry.initNativeSdk).toHaveBeenCalled();
      const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction<any>).mock.calls[0][0];
      expect(initParameter.ignoreErrorsStr).toEqual(['string1', 'string2']);
      expect(initParameter.ignoreErrorsRegex).toEqual([regex1.source, regex2.source]);
    });

    test('does not set ignoreErrorsStr or ignoreErrorsRegex if ignoreErrors is not provided', async () => {
      await NATIVE.initNativeSdk({
        dsn: 'test',
        enableNative: true,
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });
      expect(RNSentry.initNativeSdk).toHaveBeenCalled();
      const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction<any>).mock.calls[0][0];
      expect(initParameter.ignoreErrorsStr).toBeUndefined();
      expect(initParameter.ignoreErrorsRegex).toBeUndefined();
    });

    test('does not set enableLogs when option is undefined', async () => {
      await NATIVE.initNativeSdk({
        dsn: 'test',
        enableNative: true,
        autoInitializeNativeSdk: true,
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });

      expect(RNSentry.initNativeSdk).toHaveBeenCalled();
      const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction<any>).mock.calls[0][0];
      expect(initParameter.enableLogs).toBeUndefined();
    });

    it.each([
      ['without logsOrigin', undefined, true],
      ['with logsOrigin set to Native', 'native' as const, true],
      ['with logsOrigin set to all', 'all' as const, true],
      ['with logsOrigin set to JS', 'js' as const, false],
    ])('handles enableLogs %s', async (_description, logsOrigin, expectedEnableLogs) => {
      await NATIVE.initNativeSdk({
        dsn: 'test',
        enableNative: true,
        autoInitializeNativeSdk: true,
        enableLogs: true,
        ...(logsOrigin !== undefined ? { logsOrigin } : {}),
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });

      expect(RNSentry.initNativeSdk).toHaveBeenCalled();
      const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction<any>).mock.calls[0][0];
      expect(initParameter.enableLogs).toBe(expectedEnableLogs);
      expect(initParameter.logsOrigin).toBeUndefined();
    });
  });

  describe('sendEnvelope', () => {
    test('calls only captureEnvelope', async () => {
      const event = {
        event_id: 'event0',
        message: 'test',
        sdk: {
          name: 'test-sdk-name',
          version: '2.1.3',
        },
      };

      const env = createEnvelope<EventEnvelope>({ event_id: event.event_id, sent_at: '123' }, [
        [{ type: 'event' }, event] as EventItem,
      ]);

      await NATIVE.sendEnvelope(env);

      expect(RNSentry.captureEnvelope).toHaveBeenCalledWith(
        base64StringFromByteArray(
          utf8ToBytes(
            '{"event_id":"event0","sent_at":"123"}\n' +
              '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":87}\n' +
              '{"event_id":"event0","message":"test","sdk":{"name":"test-sdk-name","version":"2.1.3"}}\n',
          ),
        ),
        { hardCrashed: false },
      );
    });
    test('serializes class instances', async () => {
      class TestInstance {
        value: number = 0;
        method = (): null => null;
      }

      const event = {
        event_id: 'event0',
        sdk: {
          name: 'test-sdk-name',
          version: '2.1.3',
        },
        instance: new TestInstance(),
      };

      const env = createEnvelope<EventEnvelope>({ event_id: event.event_id, sent_at: '123' }, [
        [{ type: 'event' }, event] as EventItem,
      ]);

      await NATIVE.sendEnvelope(env);

      expect(RNSentry.captureEnvelope).toHaveBeenCalledWith(
        base64StringFromByteArray(
          utf8ToBytes(
            '{"event_id":"event0","sent_at":"123"}\n' +
              '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":93}\n' +
              '{"event_id":"event0","sdk":{"name":"test-sdk-name","version":"2.1.3"},"instance":{"value":0}}\n',
          ),
        ),
        { hardCrashed: false },
      );
    });
    test('does not call RNSentry at all if enableNative is false', async () => {
      try {
        await NATIVE.initNativeSdk({
          dsn: 'test-dsn',
          enableNative: false,
          devServerUrl: undefined,
          defaultSidecarUrl: undefined,
          mobileReplayOptions: undefined,
        });

        // @ts-expect-error for testing, does not accept an empty class.
        await NATIVE.sendEnvelope({});
      } catch (error) {
        // @ts-expect-error it is an error but it does not know the type.
        expect(error.message).toMatch('Native is disabled');
      }
      expect(RNSentry.captureEnvelope).not.toHaveBeenCalled();
    });
    test('Encloses message to an object and not introduce empty breadcrumbs on Android', async () => {
      NATIVE.platform = 'android';

      const event: Event = {
        event_id: 'event0',
        message: 'test',
      };

      const env = createEnvelope<EventEnvelope>({ event_id: event.event_id as string, sent_at: '123' }, [
        [{ type: 'event' }, event] as EventItem,
      ]);

      await NATIVE.sendEnvelope(env);

      expect(RNSentry.captureEnvelope).toHaveBeenCalledWith(
        base64StringFromByteArray(
          utf8ToBytes(
            '{"event_id":"event0","sent_at":"123"}\n' +
              '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":50}\n' +
              '{"event_id":"event0","message":{"message":"test"}}\n',
          ),
        ),
        { hardCrashed: false },
      );
    });
    test('Keeps breadcrumbs on Android if mechanism.handled is true', async () => {
      NATIVE.platform = 'android';

      const event: Event = {
        event_id: 'event0',
        exception: {
          values: [
            {
              mechanism: {
                handled: true,
                type: '',
              },
            },
          ],
        },
        breadcrumbs: [
          {
            message: 'crumb!',
          },
        ],
      };

      const env = createEnvelope<EventEnvelope>({ event_id: event.event_id as string, sent_at: '123' }, [
        [{ type: 'event' }, event] as EventItem,
      ]);

      await NATIVE.sendEnvelope(env);

      expect(RNSentry.captureEnvelope).toHaveBeenCalledWith(
        base64StringFromByteArray(
          utf8ToBytes(
            '{"event_id":"event0","sent_at":"123"}\n' +
              '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":124}\n' +
              '{"event_id":"event0","exception":{"values":[{"mechanism":{"handled":true,"type":""}}]},"breadcrumbs":[{"message":"crumb!"}]}\n',
          ),
        ),
        { hardCrashed: false },
      );
    });
    test('Keeps breadcrumbs on Android if there is no exception', async () => {
      NATIVE.platform = 'android';

      const event: Event = {
        event_id: 'event0',
        breadcrumbs: [
          {
            message: 'crumb!',
          },
        ],
      };

      const env = createEnvelope<EventEnvelope>({ event_id: event.event_id as string, sent_at: '123' }, [
        [{ type: 'event' }, event] as EventItem,
      ]);

      await NATIVE.sendEnvelope(env);

      expect(RNSentry.captureEnvelope).toHaveBeenCalledWith(
        base64StringFromByteArray(
          utf8ToBytes(
            '{"event_id":"event0","sent_at":"123"}\n' +
              '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":58}\n' +
              '{"event_id":"event0","breadcrumbs":[{"message":"crumb!"}]}\n',
          ),
        ),
        { hardCrashed: false },
      );
    });
    test('Keeps breadcrumbs on Android if mechanism.handled is false', async () => {
      NATIVE.platform = 'android';

      const event: Event = {
        event_id: 'event0',
        exception: {
          values: [
            {
              mechanism: {
                handled: false,
                type: 'onerror',
              },
            },
          ],
        },
        breadcrumbs: [
          {
            message: 'crumb!',
          },
        ],
      };

      const env = createEnvelope<EventEnvelope>({ event_id: event.event_id as string, sent_at: '123' }, [
        [{ type: 'event' }, event] as EventItem,
      ]);

      await NATIVE.sendEnvelope(env);

      expect(RNSentry.captureEnvelope).toHaveBeenCalledWith(
        base64StringFromByteArray(
          utf8ToBytes(
            '{"event_id":"event0","sent_at":"123"}\n' +
              '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":132}\n' +
              '{"event_id":"event0","exception":{"values":[{"mechanism":{"handled":false,"type":"onerror"}}]},"breadcrumbs":[{"message":"crumb!"}]}\n',
          ),
        ),
        { hardCrashed: true },
      );
    });
  });

  describe('fetchRelease', () => {
    test('fetches the release from native', async () => {
      await expect(NATIVE.fetchNativeRelease()).resolves.toMatchObject({
        build: '1.0.0.1',
        id: 'test-mock',
        version: '1.0.0',
      });
    });
  });

  describe('deviceContexts', () => {
    test('returns context object from native module on ios', async () => {
      NATIVE.platform = 'ios';

      await expect(NATIVE.fetchNativeDeviceContexts()).resolves.toMatchObject({
        someContext: {
          someValue: 0,
        },
      });

      expect(RNSentry.fetchNativeDeviceContexts).toHaveBeenCalled();
    });
    test('returns context object from native module on android', async () => {
      NATIVE.platform = 'android';

      await expect(NATIVE.fetchNativeDeviceContexts()).resolves.toMatchObject({
        someContext: {
          someValue: 0,
        },
      });

      expect(RNSentry.fetchNativeDeviceContexts).toHaveBeenCalled();
    });
  });

  describe('isModuleLoaded', () => {
    test('returns true when module is loaded', () => {
      expect(NATIVE._isModuleLoaded(RNSentry)).toBe(true);
    });
  });

  describe('crash', () => {
    test('calls the native crash', () => {
      NATIVE.nativeCrash();

      expect(RNSentry.crash).toHaveBeenCalled();
    });
    test('does not call crash if enableNative is false', async () => {
      await NATIVE.initNativeSdk({
        dsn: 'test-dsn',
        enableNative: false,
        devServerUrl: undefined,
        defaultSidecarUrl: undefined,
        mobileReplayOptions: undefined,
      });
      NATIVE.nativeCrash();

      expect(RNSentry.crash).not.toHaveBeenCalled();
    });
  });

  describe('setUser', () => {
    test('serializes all user object keys', async () => {
      NATIVE.setUser({
        email: 'hello@sentry.io',
        id: 3.14159265359,
        unique: 123,
      });

      expect(RNSentry.setUser).toHaveBeenCalledWith(
        {
          email: 'hello@sentry.io',
          id: '3.14159265359',
        },
        {
          unique: '123',
        },
      );
    });

    test('Calls native setUser with empty object as second param if no unique keys', async () => {
      NATIVE.setUser({
        id: 'Hello',
      });

      expect(RNSentry.setUser).toHaveBeenCalledWith(
        {
          id: 'Hello',
        },
        {},
      );
    });

    test('serializes user with geo data', async () => {
      NATIVE.setUser({
        id: '123',
        email: 'test@example.com',
        username: 'testuser',
        geo: {
          city: 'San Francisco',
          country_code: 'US',
          region: 'California',
        },
        customField: 'customValue',
      });

      expect(RNSentry.setUser).toHaveBeenCalledWith(
        {
          id: '123',
          email: 'test@example.com',
          username: 'testuser',
          geo: JSON.stringify({
            city: 'San Francisco',
            country_code: 'US',
            region: 'California',
          }),
        },
        {
          customField: 'customValue',
        },
      );
    });

    test('serializes user with partial geo data', async () => {
      NATIVE.setUser({
        id: '123',
        geo: {
          city: 'New York',
          country_code: 'US',
        },
      });

      expect(RNSentry.setUser).toHaveBeenCalledWith(
        {
          id: '123',
          geo: JSON.stringify({
            city: 'New York',
            country_code: 'US',
          }),
        },
        {},
      );
    });

    test('serializes user with empty geo data', async () => {
      NATIVE.setUser({
        id: '123',
        geo: {},
      });

      expect(RNSentry.setUser).toHaveBeenCalledWith(
        {
          id: '123',
          geo: '{}',
        },
        {},
      );
    });

    test('serializes user with undefined geo', async () => {
      NATIVE.setUser({
        id: '123',
        geo: undefined,
      });

      expect(RNSentry.setUser).toHaveBeenCalledWith(
        {
          id: '123',
          geo: undefined,
        },
        {},
      );
    });
  });

  describe('_processLevel', () => {
    test('converts deprecated levels', () => {
      expect(NATIVE._processLevel('log' as SeverityLevel)).toBe('debug' as SeverityLevel);
    });
    test('returns non-deprecated levels', () => {
      expect(NATIVE._processLevel('debug' as SeverityLevel)).toBe('debug' as SeverityLevel);
      expect(NATIVE._processLevel('fatal' as SeverityLevel)).toBe('fatal' as SeverityLevel);
      expect(NATIVE._processLevel('info' as SeverityLevel)).toBe('info' as SeverityLevel);
      expect(NATIVE._processLevel('warning' as SeverityLevel)).toBe('warning' as SeverityLevel);
      expect(NATIVE._processLevel('error' as SeverityLevel)).toBe('error' as SeverityLevel);
    });
  });

  describe('closeNativeSdk', () => {
    NATIVE.enableNative = true;
    test('closeNativeSdk calls native bridge', async () => {
      await NATIVE.closeNativeSdk();

      expect(RNSentry.closeNativeSdk).toHaveBeenCalled();
      expect(NATIVE.enableNative).toBe(false);
    });
  });

  describe('profiling', () => {
    test('start profiling returns true', () => {
      (RNSentry.startProfiling as jest.MockedFunction<typeof RNSentry.startProfiling>).mockReturnValue({
        started: true,
      });
      expect(NATIVE.startProfiling(true)).toBe(true);
    });
    test('failed start profiling returns false', () => {
      (RNSentry.startProfiling as jest.MockedFunction<typeof RNSentry.startProfiling>).mockReturnValue({
        error: 'error',
      });
      expect(NATIVE.startProfiling(true)).toBe(false);
    });
    test('stop profiling returns hermes profile', () => {
      (RNSentry.stopProfiling as jest.MockedFunction<typeof RNSentry.stopProfiling>).mockReturnValue({
        profile: '{ "valid": "hermes" }',
      });
      expect(NATIVE.stopProfiling()).toEqual({
        hermesProfile: { valid: 'hermes' },
      });
    });
    test('stop profiling returns hermes and native profiles', () => {
      (RNSentry.stopProfiling as jest.MockedFunction<typeof RNSentry.stopProfiling>).mockReturnValue({
        profile: '{ "valid": "hermes" }',
        nativeProfile: { valid: 'native' },
      });
      expect(NATIVE.stopProfiling()).toEqual({
        hermesProfile: { valid: 'hermes' },
        nativeProfile: { valid: 'native' },
      });
    });
    test('stop profiling returns hermes and android profiles', () => {
      (RNSentry.stopProfiling as jest.MockedFunction<typeof RNSentry.stopProfiling>).mockReturnValue({
        profile: '{ "valid": "hermes" }',
        androidProfile: { valid: 'android' },
      });
      expect(NATIVE.stopProfiling()).toEqual({
        hermesProfile: { valid: 'hermes' },
        androidProfile: { valid: 'android' },
      });
    });
    test('failed stop profiling returns null', () => {
      (RNSentry.stopProfiling as jest.MockedFunction<typeof RNSentry.stopProfiling>).mockReturnValue({
        error: 'error',
      });
      expect(NATIVE.stopProfiling()).toBe(null);
    });
    test('stop profiling returns null on invalid json profile', () => {
      (RNSentry.stopProfiling as jest.MockedFunction<typeof RNSentry.stopProfiling>).mockReturnValue({
        profile: 'invalid',
      });
      expect(NATIVE.stopProfiling()).toBe(null);
    });
  });

  describe('setExtra', () => {
    test('passes string value to native method', () => {
      NATIVE.setExtra('key', 'string value');
      expect(RNSentry.setExtra).toHaveBeenCalledWith('key', 'string value');
      expect(RNSentry.setExtra).toHaveBeenCalledOnce();
    });

    test('stringifies number value before passing to native method', () => {
      NATIVE.setExtra('key', 42);
      expect(RNSentry.setExtra).toHaveBeenCalledWith('key', '42');
      expect(RNSentry.setExtra).toHaveBeenCalledOnce();
    });

    test('stringifies boolean value before passing to native method', () => {
      NATIVE.setExtra('key', true);
      expect(RNSentry.setExtra).toHaveBeenCalledWith('key', 'true');
      expect(RNSentry.setExtra).toHaveBeenCalledOnce();
    });

    test('stringifies object value before passing to native method', () => {
      const obj = { foo: 'bar', baz: 123 };
      NATIVE.setExtra('key', obj);
      expect(RNSentry.setExtra).toHaveBeenCalledWith('key', JSON.stringify(obj));
      expect(RNSentry.setExtra).toHaveBeenCalledOnce();
    });

    test('stringifies array value before passing to native method', () => {
      const arr = [1, 'two', { three: 3 }];
      NATIVE.setExtra('key', arr);
      expect(RNSentry.setExtra).toHaveBeenCalledWith('key', JSON.stringify(arr));
      expect(RNSentry.setExtra).toHaveBeenCalledOnce();
    });

    test('handles null value by stringifying', () => {
      NATIVE.setExtra('key', null);
      expect(RNSentry.setExtra).toHaveBeenCalledWith('key', 'null');
      expect(RNSentry.setExtra).toHaveBeenCalledOnce();
    });

    test('handles undefined value by stringifying', () => {
      NATIVE.setExtra('key', undefined);
      expect(RNSentry.setExtra).toHaveBeenCalledWith('key', 'undefined');
      expect(RNSentry.setExtra).toHaveBeenCalledOnce();
    });

    test('handles non-serializable value by stringifying', () => {
      const circular: { self?: unknown } = {};
      circular.self = circular;
      NATIVE.setExtra('key', circular);
      expect(RNSentry.setExtra).toHaveBeenCalledWith('key', '{"self":"[Circular ~]"}');
      expect(RNSentry.setExtra).toHaveBeenCalledOnce();
    });
  });

  describe('setContext', () => {
    test('passes plain JS object to native method', () => {
      const context = { foo: 'bar', baz: 123 };
      NATIVE.setContext('key', context);
      expect(RNSentry.setContext).toHaveBeenCalledWith('key', context);
      expect(RNSentry.setContext).toHaveBeenCalledOnce();
    });

    test('converts non-plain JS object to plain object before passing to native method', () => {
      class TestClass {
        prop = 'value';
      }
      const context = new TestClass();
      NATIVE.setContext('key', context);
      expect(RNSentry.setContext).toHaveBeenCalledWith('key', { prop: 'value' });
      expect(RNSentry.setContext).toHaveBeenCalledOnce();
    });

    test('converts array to object with "value" key before passing to native method', () => {
      const context = [1, 'two', { three: 3 }];
      NATIVE.setContext('key', context);
      expect(RNSentry.setContext).toHaveBeenCalledWith('key', { value: [1, 'two', { three: 3 }] });
      expect(RNSentry.setContext).toHaveBeenCalledOnce();
    });

    test('converts string primitive to object with "value" key before passing to native method', () => {
      NATIVE.setContext('key', 'string value' as unknown as object);
      expect(RNSentry.setContext).toHaveBeenCalledWith('key', { value: 'string value' });
      expect(RNSentry.setContext).toHaveBeenCalledOnce();
    });

    test('converts number primitive to object with "value" key before passing to native method', () => {
      NATIVE.setContext('key', 42 as unknown as object);
      expect(RNSentry.setContext).toHaveBeenCalledWith('key', { value: 42 });
      expect(RNSentry.setContext).toHaveBeenCalledOnce();
    });

    test('converts boolean primitive to object with "value" key before passing to native method', () => {
      NATIVE.setContext('key', true as unknown as object);
      expect(RNSentry.setContext).toHaveBeenCalledWith('key', { value: true });
      expect(RNSentry.setContext).toHaveBeenCalledOnce();
    });

    test('handles null value by passing null to native method', () => {
      NATIVE.setContext('key', null);
      expect(RNSentry.setContext).toHaveBeenCalledWith('key', null);
      expect(RNSentry.setContext).toHaveBeenCalledOnce();
    });

    test('handles undefined value by converting to object with "value" key', () => {
      NATIVE.setContext('key', undefined as unknown as object);
      expect(RNSentry.setContext).toHaveBeenCalledWith('key', { value: undefined });
      expect(RNSentry.setContext).toHaveBeenCalledOnce();
    });

    test('handles non-serializable value by converting to normalized object', () => {
      const circular: { self?: unknown } = {};
      circular.self = circular;
      NATIVE.setContext('key', circular);
      expect(RNSentry.setContext).toHaveBeenCalledWith('key', { self: '[Circular ~]' });
      expect(RNSentry.setContext).toHaveBeenCalledOnce();
    });
  });

  describe('crashedLastRun', () => {
    test('return true when promise resolves true ', async () => {
      (RNSentry.crashedLastRun as jest.Mock).mockResolvedValue(true);

      const result = await NATIVE.crashedLastRun();
      expect(result).toBeTrue();
    });

    test('return true when promise resolves false ', async () => {
      (RNSentry.crashedLastRun as jest.Mock).mockResolvedValue(false);

      const result = await NATIVE.crashedLastRun();
      expect(result).toBeFalse();
    });

    test('return null when promise does not resolve ', async () => {
      (RNSentry.crashedLastRun as jest.Mock).mockResolvedValue(undefined);

      const result = await NATIVE.crashedLastRun();
      expect(result).toBeNull();
    });
  });

  describe('primitiveProcessor and _setPrimitiveProcessor', () => {
    describe('primitiveProcessor', () => {
      it('default primitiveProcessor returns value as string', () => {
        expect(NATIVE.primitiveProcessor('test')).toBe('test');
        expect(NATIVE.primitiveProcessor(123)).toBe(123);
        expect(NATIVE.primitiveProcessor(true)).toBe(true);
        expect(NATIVE.primitiveProcessor(null)).toBe(null);
        expect(NATIVE.primitiveProcessor(undefined)).toBe(undefined);
      });

      it('handles all primitive types correctly', () => {
        const testCases = [
          { input: 'string', expected: 'string' },
          { input: 42, expected: 42 },
          { input: true, expected: true },
          { input: false, expected: false },
          { input: null, expected: null },
          { input: undefined, expected: undefined },
          { input: BigInt(123), expected: BigInt(123) },
        ];

        testCases.forEach(({ input, expected }) => {
          expect(NATIVE.primitiveProcessor(input)).toBe(expected);
        });
      });
    });

    describe('_setPrimitiveProcessor', () => {
      it('sets primitiveProcessor to the provided function', () => {
        const mockProcessor = jest.fn(value => `processed_${value}`);

        NATIVE._setPrimitiveProcessor(mockProcessor);

        expect(NATIVE.primitiveProcessor).toBe(mockProcessor);
      });

      it('allows custom processing of primitive values', () => {
        const customProcessor = (value: any) => {
          if (typeof value === 'boolean') {
            return value ? 'YES' : 'NO';
          }
          if (value === null) {
            return 'NULL';
          }
          return String(value);
        };

        NATIVE._setPrimitiveProcessor(customProcessor);

        expect(NATIVE.primitiveProcessor(true)).toBe('YES');
        expect(NATIVE.primitiveProcessor(false)).toBe('NO');
        expect(NATIVE.primitiveProcessor(null)).toBe('NULL');
        expect(NATIVE.primitiveProcessor(42)).toBe('42');
        expect(NATIVE.primitiveProcessor('test')).toBe('test');
      });

      it('can be chained with PrimitiveToString for consistent formatting', () => {
        const { PrimitiveToString } = require('../src/js/utils/primitiveConverter');

        NATIVE._setPrimitiveProcessor(PrimitiveToString);

        expect(NATIVE.primitiveProcessor(true)).toBe('True');
        expect(NATIVE.primitiveProcessor(false)).toBe('False');
        expect(NATIVE.primitiveProcessor(null)).toBe('');
        expect(NATIVE.primitiveProcessor(42)).toBe('42');
        expect(NATIVE.primitiveProcessor('test')).toBe('test');
        expect(NATIVE.primitiveProcessor(undefined)).toBeUndefined();
      });

      it('can be reset to default behavior', () => {
        const customProcessor = jest.fn();
        NATIVE._setPrimitiveProcessor(customProcessor);
        expect(NATIVE.primitiveProcessor).toBe(customProcessor);

        const defaultProcessor = (value: any) => value;
        NATIVE._setPrimitiveProcessor(defaultProcessor);
        expect(NATIVE.primitiveProcessor).toBe(defaultProcessor);
      });

      it('works with primitiveTagIntegration', () => {
        const { primitiveTagIntegration } = require('../src/js/integrations/primitiveTagIntegration');

        const client = {
          on: jest.fn(),
        };

        const integration = primitiveTagIntegration();
        integration.setup(client);
        integration.afterAllSetup();

        expect(NATIVE.primitiveProcessor(true)).toBe('True');
        expect(NATIVE.primitiveProcessor(false)).toBe('False');
        expect(NATIVE.primitiveProcessor(null)).toBe('');
      });
    });
  });
});
